/**
 * @particulars 转化ast 语法树
 */

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;

// 匹配的是开始的标签名：<div>
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 匹配的是自闭合标签：<div>、<div />
const startTagClose = /^\s*(\/?)>/;
// 匹配的是结束的标签名：</?>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// 匹配标签属性的：第一个分组匹配的是属性的key、后续的分组匹配的是属性的value
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

export function parseHTML(html) {
  const ELEMENT_TYPE = 1; // 标识元素节点
  const TEXT_TYPE = 3; // 标识文本节点
  const stack = []; // 存放数据的栈
  let currentParent; // 当前节点的父节点
  let root; // 根元素

  // 抽象ast 语法树
  function createAstElement(tag, attrs) {
    return {
      tag, // 节点标签
      type: ELEMENT_TYPE, // 节点类型
      children: [], // 子节点
      attrs, // 属性集合
      parent: null // 父节点
    }
  }

  // 2-2、处理开始标签的数据
  function start(tag, attrs) {
    // 创建ast 节点
    const node = createAstElement(tag, attrs);
    // 如果root 为空，则当前的节点为树的根节点
    if (!root) root = node;
    // 如果父节点存在，给当前节点指定父节点和子节点
    if (currentParent) {
      node.parent = currentParent;
      currentParent.children.push(node);
    }
    stack.push(node);
    // currentParent 为栈中最后一个，表示下一栈元素的父节点
    currentParent = node;
  }

  // 2-3、处理文本节点数据
  function charts(text) {
    // 去除空格，给当前节点添加文本数据
    text = text.replace(/\s/g, '');
    text && currentParent.children.push({
      type: TEXT_TYPE,
      text
    });
  }

  // 2-4、处理结束标签数据
  function end(tag) {
    // 弹出栈中最后一个元素，并重置当前节点的父节点
    const lastTag = stack.pop();
    // 检验标签的匹配是否合理，单标签<hr /> 会被el.outerHTML 解析成<hr>
    if (lastTag.tag !== tag) console.error('标签元素有问题：', lastTag.tag);
    currentParent = stack.at(-1);
  }

  // 从html 中删除匹配到的内容
  function advance(str) {
    html = html.slice(str.length);
  }

  // 2-1、处理开始标签，匹配完就删除然后往下继续匹配
  function parseStartTag() {
    const start = html.match(startTagOpen);

    if (start) {
      const match = {
        tagName: start[1],
        attrs: []
      };

      // 匹配完就删除
      advance(start[0]);

      // 如果不是开始标签的结束标签，就一直匹配下去
      let attr, endTags;

      while (!(endTags = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0]);

        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5] || true
        });
      }

      if (endTags) advance(endTags[0]);
      return match;
    }

    // 不是开始标签则返回false
    return false;
  }

  while (html) {
    // textEnd 为0 表示是一个开始标签或者是结束标签；>0 说明是文本的结束位置：<div> hello </div>
    const textEnd = html.indexOf('<');

    // 解析到开始标签或者是结束标签
    if (textEnd === 0) {
      // 解析到的开始标签
      const startTagMatch = parseStartTag(html);

      if (startTagMatch) {
        // 处理开始标签
        start(startTagMatch.tagName, startTagMatch.attrs);
        continue;
      }

      // 解析到的结束标签
      const endTagMatch = html.match(endTag);
      if (endTagMatch) {
        // 处理完结束标签就删除，并跳过本次循环，下面的文本匹配也没有必要了
        end(endTagMatch[1]);
        advance(endTagMatch[0]);
        continue;
      }
    }

    // 解析到的文本内容
    if (textEnd > 0) {
      const text = html.slice(0, textEnd);

      if (text) {
        // 处理完文本节点就删除
        charts(text);
        advance(text);
      }
    }
  }

  return root;
}
